var util = require('util'),
    fs   = require('fs');

/**
 * Utility functions for the CWContact class.
 */
var helpers = {};

/**
 * Checks whether a paramter is empty.
 *
 * @type {*} Value to perform the checks on.
 * @return {Boolean} Whether the value is empty.
 */
helpers.isEmpty = function (value) {
    if (typeof value === 'undefined') {
        throw new TypeError('Value must be defined');
    }

    if (typeof value === 'boolean') {
        throw new TypeError('Cannot evaluate the emptiness of a boolean value');
    }

    if (value === null) {
        throw new TypeError('Cannot evaluate the emptiness of a null value');
    }

    if (typeof value === 'string' || typeof value === 'number' || util.isArray(value)) {
        return value.length === 0;
    }

    if (typeof value === 'object') {
        return Object.keys(value).length === 0;
    }

    return false;
};

/**
 * Checks whether two arrays contain the same content.
 *
 * @return {boolean} Whether the arrasy are equals
 */
helpers.arraysAreEqual = function (arr1, arr2) {
    if (!util.isArray(arr1) || !util.isArray(arr2)) {
        throw new TypeError('Parameters must be arrays');
    }

    if (arr1.length !== arr2.length) {
        return false;
    }

    for (var i = 0; i < arr1.length ; i++) {
        if(arr1[i] !== arr2[i]) {
            return false;
        }
    }

    return true;
};

/**
 * The parseVCard method extracts the relevant information from a single vcard.
 *
 * VCard example :
 * BEGIN:VCARD
 *   VERSION:3.0
 *   N:”<nom>”
 *   FN:”<nom_complet>”
 *   ORG:”<organisation>”
 *   TITLE:”<fonction>”
 *   TEL;type=WORK:”<numéro_téléphone>”
 *   EMAIL;TYPE=PREF,INTERNET:”<email>“
 * END:VCARD
 *
 *
 *
 * @param  {string} vcard to parse.
 * @return {object} parsed informations.
 *
 */
helpers.parseVCard = function parseVCard (vcard) {
    if (typeof vcard !== 'string') {
        throw new TypeError('A vCard must be a string');
    }

    if (!/^BEGIN:VCARD/.test(vcard)) {
        throw new SyntaxError('BEGIN tag missing');
    }

    if (!/VERSION:3.0/.test(vcard)) {
        throw new SyntaxError('VERSION tag missing');
    }

    if (!/END:VCARD/.test(vcard)) {
        throw new SyntaxError('END tag missing');
    }

    var info = {};

    var regexes = {
        name: /\nN:"(.+)"\n/,
        fullname: /\nFN:"(.+)"\n/,
        address: /\nADDRESS:"(.+)"\n/,
        organisation: /\nORG:"(.+)"\n/,
        title: /\nTITLE:"(.+)"\n/,
        tel : /\nTEL(?:.+):"(.+)"\n/,
        mail: /\nEMAIL(?:.+):"(.+)"\n/
    };

    for (var key in regexes) {
        var res = regexes[key].exec(vcard);
        if (res) {
            info[key] = res[1];
        }
    }

    return info;
};

/**
 * Domain class. Contains all the necessary information to create/update/delete/export a contact.
 *
 * @constructor
 */
function CWContact () {
    var defaultValues;

    defaultValues = {
        name:         '',
        firstname:    '',
        fullname:     '',
        address:      '',
        organisation: [],
        title:        [],
        mail:         '',
        tel:          ''
    };

    this._name         = defaultValues.name;
    this._firstname    = defaultValues.firstname;
    this._fullname     = defaultValues.fullname;
    this._address      = defaultValues.address;
    this._organisation = defaultValues.organisation;
    this._title        = defaultValues.title;
    this._mail         = defaultValues.mail;
    this._tel          = defaultValues.tel;
}

/**
 * Getter/Setter for a CWContact name.
 *
 * @param name When provided, this method sets name as the new CWContact instance name.
 * @returns {*} Returns the CWContact instance the _name attribute refers to when a name is provided.
 * When it is not, it directly returns the instance name value.
 */
CWContact.prototype.name = function name (name) {
    if (name === undefined) {
        return this._name;
    }

    if (name && typeof name !== 'string') {
        throw new TypeError('Name must be a string.');
    }

    this._name = name;

    return this;
};

/**
 * Getter/Setter for a CWContact firstname.
 *
 * @param firstname When provided, this method sets firstname as the new CWContact instance firstname.
 * @returns {*} Returns the CWContact instance the _firstname attribute refers to when a firstname is provided.
 * When it is not, it directly returns the instance firstname value.
 */
CWContact.prototype.firstname = function firstname (firstname) {
    if (firstname === undefined) {
        return this._firstname;
    }

    if (firstname && typeof firstname !== 'string') {
        throw new TypeError('Firstname must be a string.');
    }

    this._firstname = firstname;

    return this;
};

/**
 * Getter/Setter for a CWContact fullname.
 *
 * @param fullname When provided, this method sets fullname as the new CWContact instance fullname.
 * @returns {*} Returns the CWContact instance the _fullname attribute refers to when a fullname is provided.
 * When it is not, it directly returns the instance fullname value.
 */
CWContact.prototype.fullname = function fullname (fullname) {
    if (fullname=== undefined) {
        return this._fullname;
    }

    if (fullname && typeof fullname !== 'string') {
        throw new TypeError('Fullname must be a string.');
    }

    this._fullname = fullname;

    return this;
};

/**
 * Getter/Setter for a CWContact address.
 *
 * @param address When provided, this method sets address as the new CWContact instance address.
 * @returns {*} Returns the CWContact instance the _address attribute refers to when an address is provided.
 * When it is not, it directly returns the instance address value.
 */
CWContact.prototype.address = function address (address) {
    if (address === undefined) {
        return this._address;
    }

    if (address && typeof address !== 'string') {
        throw new TypeError('Address must be a string.');
    }

    this._address = address;

    return this;
};

/**
 * Getter/Setter for a CWContact organisation.
 *
 * @param organisation When provided, this method sets organisation as the new CWContact instance organisation.
 * @returns {*} Returns the CWContact instance the _organisation attribute refers to when an organisation is provided.
 * When it is not, it directly returns the instance organisation value.
 */
CWContact.prototype.organisation = function organisation (organisation) {
    if (organisation === undefined) {
        return this._organisation;
    }

    if (!util.isArray(organisation)) {
        throw new TypeError('Organisation must be an array.');
    }

    this._organisation = organisation;

    return this;
};

/**
 * Getter/Setter for a CWContact title.
 *
 * @param title When provided, this method sets title as the new CWContact instance title .
 * @returns {*} Returns the CWContact instance the _title attribute refers to when a title is provided.
 * When it is not, it directly returns the instance title value.
 */
CWContact.prototype.title = function title (title) {
    if (title=== undefined) {
        return this._title;
    }

    if (!util.isArray(title)) {
        throw new TypeError('Title must be an array.');
    }

    this._title= title;

    return this;
};

/**
 * Getter/Setter for a CWContact mail.
 *
 * @param mail When provided, this method sets mail as the new CWContact instance mail.
 * @returns {*} Returns the CWContact instance the _mail attribute refers to when a mail is provided.
 * When it is not, it directly returns the instance mail value.
 */
CWContact.prototype.mail = function mail (mail) {
    if (mail === undefined) {
        return this._mail;
    }

    if (mail && typeof mail !== 'string') {
        throw new TypeError('Mail must be a string.');
    }

    this._mail = mail;

    return this;
};

/**
 * Getter/Setter for a CWContact tel.
 *
 * @param tel When provided, this method sets tel as the new CWContact
 * instance tel.
 * @returns {*} Returns the CWContact instance the _tel attribute refers to
 * when a tel is provided. When it is not, it directly returns the instance
 * tel value.
 */
CWContact.prototype.tel = function tel (tel) {
    if (tel === undefined) {
        return this._tel;
    }

    if (tel && typeof tel !== 'string') {
        throw new TypeError('Tel must be a string.');
    }

    this._tel = tel;

    return this;
};

/**
 * Merge a cwcontact into another one.
 * Fill the lacking information. The information remains unchanged in case
 * the field is already filled.
 *
 * @param  {CWContact} Contact to import the information from.
 * @return {CWContact} Returns the current object after merging.
 */
CWContact.prototype.merge = function merge (contact) {
    if (!(contact instanceof CWContact)) {
        throw new TypeError('Contact must be a CWContact instance');
    }

    if (typeof contact === 'undefined') {
        throw new TypeError('Contact must be defined');
    }

    if (contact === null) {
        throw new Error('Contact can not be null');
    }

    for (var attr in this) {
        if (this.hasOwnProperty(attr)) {
            if (helpers.isEmpty(this[attr]) && !helpers.isEmpty(contact[attr])) {
                var methodName = attr.slice(1);
                this[methodName](contact[attr]);
            }
        }
    }

    return this;
};

/**
 * Compare two CWContact instances. Return the differences as an object.
 *
 * @param  {CWContact} contact to compare with.
 * @return {Object} Object if differences
 */
CWContact.prototype.compare = function compare (contact) {
    if (!(contact instanceof CWContact)) {
        throw new TypeError('Contact must be a CWContact instance');
    }

    var diff = {};
    for (var attr in this) {
        var key = attr.slice(1);
        if (typeof this[attr] === 'string') {
            if (this[attr] !== contact[attr]) {
                diff[key] = contact[attr];
            }
        }

        if (util.isArray(this[attr])) {
            if (!helpers.arraysAreEqual(this[attr], contact[attr])) {
                diff[key] = contact[attr];
            }
        }
    }

    return diff;
};

/**
 * The toCSV method returns the attributes of the current object in the the CSV format.
 *
 * @syntax myCWContact.toCSV()
 * @return {string} CSV formatted string of the current object attributes.
 */
CWContact.prototype.toCSV = function toCSV (toFile) {
    var csv = [];
    for (var attr in this){
        if (this.hasOwnProperty(attr)) {
            var val = this[attr];
            csv.push(util.isArray(val) ? val.join(',') : val);
        }
    }

    // A semicolon in the string will break the csv structure, hence the forbidding
    if (csv.join('').indexOf(';') > -1) {
        throw new SyntaxError('The ";" character is not authorized');
    }

    var csvString = csv.join(';');

    if(toFile) {
        fs.writeFileSync(this.fullname()+'.csv', csvString);
    }

    return csvString;
};

/**
 * The fromVCard method parses the vcard string and assigns the values to the current object.
 *
 * @param  {string} vcard string to parse
 * @return {CWContact}    current object
 */
CWContact.prototype.fromVCard = function fromVCard (vcard) {
    if (typeof vcard !== 'string') {
        throw new TypeError('VCards must be a string');
    }

    var info = helpers.parseVCard(vcard);

    for (var key in info) {
        var val = info[key];
        if (key === 'title' || key === 'organisation') {
            val = info[key].split(',').map(function(item){return item.trim();});
        }

        this[key](val);
    }

    return this;
};

module.exports = {
    CWContact: CWContact,
    helpers: helpers
};
